
import os

import mglearn.plots
import numpy as np
from matplotlib import pyplot as plt
from pandas.core.common import random_state
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs, fetch_lfw_people, make_moons
from sklearn.decomposition import NMF, PCA
from sklearn.model_selection import train_test_split

# k均值聚类
'''
k 均值聚类是一种简单且常用的聚类算法。它通过以下步骤寻找簇中心：
 1.将每个数据点分配给最近的簇中心；
 2.将每个簇中心更新为分配给它的所有数据点的平均值。
   当簇的分配不再变化时，算法结束。
'''

# 下面的例子在一个模拟数据集上对这一算法进行说明：
mglearn.plots.plot_kmeans_algorithm()
# plt.show()

'''
簇中心用三角形表示，数据点用圆形表示，颜色区分不同的簇。我们指定寻找三个簇，算法通过随机选择三个数据点作为初始簇中心开始（“初始化”）。然后，算法迭代执行以下步骤：
 1.将每个数据点分配给最近的簇中心（“分配数据点”）。
 2.将簇中心更新为分配给它的点的平均值（“重新计算中心”）。
   重复这个过程两次后，第三次迭代时簇中心的分配不再变化，算法结束。
'''

# 用 scikit-learn 应用 k 均值相当简单。下面我们将其应用于上图中的模拟数据。
# 我们将KMeans类实例化，并设置我们要寻找的簇个数 3。然后对数据调用 fit 方法

# 生成模拟的二维数据
X, y = make_blobs(random_state=1)
# 构建聚类模型
kmeans = KMeans(n_clusters=3)
kmeans.fit(X)
# 如果不指定 n_clusters，它的默认值是 8。

# 算法运行期间，为 X 中的每个训练数据点分配一个簇标签。你可以在 kmeans.labels_ 属性
# 中找到这些标签

print("Cluster memberships:\n{}".format(kmeans.labels_))

# 也可以用 predict 方法为新数据点分配簇标签。预测时会将最近的簇中心分配给每个新
# 数据点，但现有模型不会改变。对训练集运行 predict 会返回与 labels_ 相同的结果

print(kmeans.predict(X))

'''
聚类算法和分类算法类似，每个元素都有一个标签，但聚类的标签没有先验意义。
例如，在人脸图像聚类中，第3个簇可能只包含朋友Bela的面孔，但数字“3”是随机的，只有查看图片后才知道簇的内容。
对于二维数据集，簇的编号（如0或1）也没有固定意义，因为聚类算法的初始化是随机的，重新运行可能会得到不同的编号。
'''

# 下面又给出了这个数据的图像（图 3-25）。簇中心被保存在 cluster_centers_ 属性中，我们用三角形表示它们：

mglearn.discrete_scatter(X[:, 0], X[:, 1], kmeans.labels_, markers='o')
mglearn.discrete_scatter(
 kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], [0, 1, 2],
 markers='^', markeredgewidth=2)

# plt.show()

# 我们也可以使用更多或更少的簇中心
fig,axes = plt.subplots(1,2,figsize=(10,5))
# 使用2个簇中心
kmeans = KMeans(n_clusters=2)
kmeans.fit(X)
assignments = kmeans.labels_
mglearn.discrete_scatter(X[:, 0], X[:, 1], assignments, ax=axes[0])

# 使用5个簇中心
kmeans = KMeans(n_clusters=5)
kmeans.fit(X)
assignments = kmeans.labels_
mglearn.discrete_scatter(X[:, 0], X[:, 1], assignments, ax=axes[0])

# plt.show()

# k均值的失败案例

'''
即使你知道数据集中簇的“正确”个数，k 均值聚类也可能无法找到它们。
因为 k 均值假设每个簇是凸形的（由中心定义），并且所有簇具有相似的“直径”，边界总是画在簇中心的中间。
这使得 k 均值只能找到相对简单的形状，有时会得到意外的结果。
'''

X_varied, y_varied = make_blobs(n_samples=200,
 cluster_std=[1.0, 2.5, 0.5],
 random_state=170)
y_pred = KMeans(n_clusters=3, random_state=0).fit_predict(X_varied)
mglearn.discrete_scatter(X_varied[:, 0], X_varied[:, 1], y_pred)
plt.legend(["cluster 0", "cluster 1", "cluster 2"], loc='best')
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
# plt.show()

'''
你可能会认为，左下方的密集区域是第一个簇，右上方的密集区域是第二个，中间密度较
小的区域是第三个。但事实上，簇 0 和簇 1 都包含一些远离簇中其他点的点。
'''

'''
k 均值假设每个簇在所有方向上都同等重要。对于沿对角线拉长的二维数据集，尽管数据明显分为三部分，k 均值可能无法正确处理，
因为它只考虑数据点到簇中心的最近距离，无法适应这种方向上的变化。
'''
# 生成一些随机分组数据
X, y = make_blobs(random_state=170, n_samples=600)
rng = np.random.RandomState(74)
# 变换数据使其拉长
transformation = rng.normal(size=(2, 2))
X = np.dot(X, transformation)
# 将数据聚类成3个簇
kmeans = KMeans(n_clusters=3)
kmeans.fit(X)
y_pred = kmeans.predict(X)
# 画出簇分配和簇中心
plt.scatter(X[:, 0], X[:, 1], c=y_pred, cmap=mglearn.cm3)
plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
 marker='^', c=[0, 1, 2], s=100, linewidth=2, cmap=mglearn.cm3)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
# plt.show()


'''
如果簇的形状更加复杂,那么 k 均值的表现也很差
'''

# 矢量量化，或者将k均值看作分解
'''
k 均值是一种聚类算法，但它与分解方法（如 PCA 和 NMF）有相似之处。
PCA 寻找数据中方差最大的方向，
NMF 寻找数据的“极值”或“部分”，并将数据点表示为分量之和。
而 k 均值则用簇中心来表示每个数据点，相当于每个点只用一个分量（簇中心）来表示。
这种观点将 k 均值视为一种分解方法，称为矢量量化（vector quantization）。

'''

# 我们来并排比较 PCA、NMF 和 k 均值，分别显示提取的分量，以及利用 100 个分量对测试集中人脸的重建。
# 对于 k 均值，重建就是在训练集中找到的最近的簇中心：
data_home = os.path.join('/', 'data')
print(data_home)
people = fetch_lfw_people(min_faces_per_person=20, resize=0.7,data_home=data_home,download_if_missing=True)
image_shape = people.images[0].shape
# plt.show()

# 但这个数据集有些偏斜，其中包含 George W. Bush（小布什）和 Colin Powell（科林 • 鲍威尔）的大量图像
# 为了降低数据偏斜，我们对每个人最多只取 50 张图像（否则，特征提取将会被 George W.Bush 的可能性大大影响）
mask = np.zeros(people.target.shape, dtype=np.bool)
for target in np.unique(people.target):
 mask[np.where(people.target == target)[0][:50]] = 1
X_people = people.data[mask]
y_people = people.target[mask]
# 将灰度值缩放到0到1之间，而不是在0到255之间
# 以得到更好的数据稳定性
X_people = X_people / 255.

X_train,X_test,y_train,y_test = train_test_split(X_people,y_people,stratify=y_people,random_state=0)
nmf = NMF(n_components=100,random_state=0)
nmf.fit(X_train)
pca = PCA(n_components=100,random_state=0)
pca.fit(X_train)
kmeans = KMeans(n_clusters=100,random_state=0)
kmeans.fit(X_train)

X_reconstructed_pca = pca.inverse_transform(pca.transform(X_test))
X_reconstructed_kmeans = kmeans.cluster_centers_[kmeans.predict(X_test)]
X_reconstructed_nmf = np.dot(nmf.transform(X_test), nmf.components_)

fig, axes = plt.subplots(3, 5, figsize=(8, 8),
 subplot_kw={'xticks': (), 'yticks': ()})
fig.suptitle("Extracted Components")
for ax, comp_kmeans, comp_pca, comp_nmf in zip(
 axes.T, kmeans.cluster_centers_, pca.components_, nmf.components_):
 ax[0].imshow(comp_kmeans.reshape(image_shape))
 ax[1].imshow(comp_pca.reshape(image_shape), cmap='viridis')
 ax[2].imshow(comp_nmf.reshape(image_shape))
axes[0, 0].set_ylabel("kmeans")
axes[1, 0].set_ylabel("pca")
axes[2, 0].set_ylabel("nmf")
fig, axes = plt.subplots(4, 5, subplot_kw={'xticks': (), 'yticks': ()},
 figsize=(8, 8))
fig.suptitle("Reconstructions")
for ax, orig, rec_kmeans, rec_pca, rec_nmf in zip(
 axes.T, X_test, X_reconstructed_kmeans, X_reconstructed_pca,
 X_reconstructed_nmf):
 ax[0].imshow(orig.reshape(image_shape))
 ax[1].imshow(rec_kmeans.reshape(image_shape))
 ax[2].imshow(rec_pca.reshape(image_shape))
 ax[3].imshow(rec_nmf.reshape(image_shape))
axes[0, 0].set_ylabel("original")
axes[1, 0].set_ylabel("kmeans")
axes[2, 0].set_ylabel("pca")
axes[3, 0].set_ylabel("nmf")

# plt.show()

# 利用 100 个分量（或簇中心）的 k 均值、PCA 和 NMF 的图像重建的对比——k 均值的每
# 张图像中仅使用了一个簇中心

'''
用 k 均值做矢量量化的一个优点是可以用比输入维度更多的簇来编码数据。
以“two_moons”数据为例，它只有两个维度，用 PCA 或 NMF 降到一维会破坏数据结构。
但 k 均值可以通过增加簇中心的数量，找到一种更有表现力的数据表示方式。
'''
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
X,y = make_moons(n_samples=200, noise=0.05, random_state=0)
kmeans = KMeans(n_clusters=10,random_state=0)
kmeans.fit(X)
y_pred = kmeans.predict(X)
plt.scatter(X[:, 0], X[:, 1], c=y_pred, s=60,cmap='Paired')
plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],s=60,marker='^', c=range(kmeans.n_clusters), linewidth=2,cmap='Paired')
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
print("Cluster memberships:\n{}".format(y_pred))
plt.show()

'''
我们使用了 10 个簇中心，也就是说，现在每个点都被分配了 0 到 9 之间的一个数字。我们
可以将其看作 10 个分量表示的数据（我们有 10 个新特征），只有表示该点对应的簇中心的
那个特征不为 0，其他特征均为 0。利用这个 10 维表示，现在可以用线性模型来划分两个
半月形，而利用原始的两个特征是不可能做到这一点的。将到每个簇中心的距离作为特征，
还可以得到一种表现力更强的数据表示。可以利用 kmeans 的 transform 方法来完成这一点：
'''

'''
k 均值是一种流行的聚类算法，优点是容易理解、实现简单且运行速度快。它还可以扩展到大型数据集，例如 scikit-learn 中的 MiniBatchKMeans 变体，能够处理非常大的数据集。
然而，k 均值也有缺点：
 1.它依赖于随机初始化，结果可能因随机种子不同而有所差异。scikit-learn 默认会用 10 种随机初始化运行算法，并返回最佳结果。
 2.k 均值对簇的形状有较强假设（例如假设簇是凸形且大小相似），并且需要用户提前指定簇的数量，这在实际应用中可能难以确定。
'''

